Passed
Push — feature/add-2fa-support ( 23e818...85b1f3 )
by Chris
24:19 queued 11:16
created

ApplicationReviewWithNav   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 278
dl 0
loc 337
rs 10
c 0
b 0
f 0
wmc 21

6 Functions

Rating   Name   Duplication   Size   Complexity  
C render 0 204 5
A showNotes 0 21 4
A isUnchanged 0 12 3
B handleSaveClicked 0 44 5
A handleStatusChange 0 9 2
A handleLinkClicked 0 11 2
1
import React from "react";
2
import {
3
  injectIntl,
4
  WrappedComponentProps,
5
  FormattedMessage,
6
  defineMessages,
7
} from "react-intl";
8
import className from "classnames";
9
import Swal from "sweetalert2";
10
import * as routes from "../../helpers/routes";
11
import Select, { SelectOption } from "../Select";
12
import { Application } from "../../models/types";
13
import { ReviewStatusId } from "../../models/lookupConstants";
14
15
const messages = defineMessages({
16
  veteranLogo: {
17
    id: "veteranStatus.veteranLogoAlt",
18
    defaultMessage: "Talent cloud veteran logo",
19
    description: "Alt Text for Veteran Logo Img",
20
  },
21
  emailCandidate: {
22
    id: "apl.emailCandidateLinkTitle",
23
    defaultMessage: "Email this candidate.",
24
    description: "Title, hover text, for email link.",
25
  },
26
  viewApplicationTitle: {
27
    id: "apl.viewApplicationLinkTitle",
28
    defaultMessage: "View this applicant's application.",
29
    description: "Title, hover text, for View Application Link",
30
  },
31
  viewProfileTitle: {
32
    id: "apl.viewProfileLinkTitle",
33
    defaultMessage: "View this applicant's profile.",
34
    description: "Title, hover text, for View Profile Link",
35
  },
36
  decision: {
37
    id: "apl.decision",
38
    defaultMessage: "Decision",
39
    description: "Decision dropdown label",
40
  },
41
  notReviewed: {
42
    id: "reviewStatus.notReviewed",
43
    defaultMessage: "Not Reviewed",
44
    description: "Decision dropdown label",
45
  },
46
  saving: {
47
    id: "button.saving",
48
    defaultMessage: "Saving...",
49
    description: "Dynamic Save button label",
50
  },
51
  save: {
52
    id: "button.save",
53
    defaultMessage: "Save",
54
    description: "Dynamic Save button label",
55
  },
56
  saved: {
57
    id: "button.saved",
58
    defaultMessage: "Saved",
59
    description: "Dynamic Save button label",
60
  },
61
  addNote: {
62
    id: "apl.addNote",
63
    defaultMessage: "+ Add a Note",
64
    description: "Dynamic Note button label",
65
  },
66
  editNote: {
67
    id: "apl.editNote",
68
    defaultMessage: "Edit Note",
69
    description: "Dynamic Note button label",
70
  },
71
  screenedOut: {
72
    id: "reviewStatus.screenedOut",
73
    defaultMessage: "Screened Out",
74
    description: "Dynamic Note button label",
75
  },
76
  cancelButton: {
77
    id: "button.cancel",
78
    defaultMessage: "Cancel",
79
    description: "Cancel button label",
80
  },
81
  stillThinking: {
82
    id: "reviewStatus.stillThinking",
83
    defaultMessage: "Still Thinking",
84
    description: "Dynamic Note button label",
85
  },
86
  stillIn: {
87
    id: "reviewStatus.stillIn",
88
    defaultMessage: "Still In",
89
    description: "Dynamic Note button label",
90
  },
91
  confirmButton: {
92
    id: "button.confirm",
93
    defaultMessage: "Confirm",
94
    description: "Confirm button for modal dialogue boxes",
95
  },
96
  screenOutConfirm: {
97
    id: "apl.screenOutConfirm",
98
    defaultMessage: "Screen out the candidate?",
99
    description: "Are you sure you want to screen out the candidate warning",
100
  },
101
  screenInConfirm: {
102
    id: "apl.screenInConfirm",
103
    defaultMessage: "Screen the candidate back in?",
104
    description: "Are you sure you want to screen in the candidate warning",
105
  },
106
});
107
108
interface ApplicationReviewWithNavProps {
109
  application: Application;
110
  reviewStatusOptions: SelectOption[];
111
  onStatusChange: (
112
    applicationId: number,
113
    statusId: number | null,
114
  ) => Promise<void>;
115
  onNotesChange: (applicationId: number, notes: string | null) => void;
116
  isSaving: boolean;
117
}
118
119
interface ApplicationReviewWithNavState {
120
  selectedStatusId: number | undefined;
121
}
122
123
class ApplicationReviewWithNav extends React.Component<
124
  ApplicationReviewWithNavProps & WrappedComponentProps,
125
  ApplicationReviewWithNavState
126
> {
127
  public constructor(
128
    props: ApplicationReviewWithNavProps & WrappedComponentProps,
129
  ) {
130
    super(props);
131
    this.state = {
132
      selectedStatusId:
133
        props.application.application_review &&
134
        props.application.application_review.review_status_id
135
          ? props.application.application_review.review_status_id
136
          : undefined,
137
    };
138
    this.handleStatusChange = this.handleStatusChange.bind(this);
139
    this.handleSaveClicked = this.handleSaveClicked.bind(this);
140
    this.showNotes = this.showNotes.bind(this);
141
    this.handleLinkClicked = this.handleLinkClicked.bind(this);
142
    this.isUnchanged = this.isUnchanged.bind(this);
143
  }
144
145
  /**
146
   * Returns true only if selectedStatusId matches the review
147
   * status of props.application
148
   */
149
  protected isUnchanged = (): boolean => {
150
    const { application } = this.props;
151
    const { selectedStatusId } = this.state;
152
    if (
153
      application.application_review &&
154
      application.application_review.review_status_id
155
    ) {
156
      return (
157
        application.application_review.review_status_id === selectedStatusId
158
      );
159
    }
160
    return selectedStatusId === undefined;
161
  };
162
163
  /**
164
   * When save is clicked, it is only necessary to save the status
165
   * @param event
166
   */
167
  protected handleSaveClicked(): Promise<void> {
168
    const { selectedStatusId } = this.state;
169
    const { application, onStatusChange, intl } = this.props;
170
    const status = selectedStatusId || null;
171
172
    const sectionChange = (
173
      oldStatus: number | null,
174
      newStatus: number | null,
175
    ): boolean => {
176
      const oldIsScreenedOut: boolean =
177
        oldStatus === ReviewStatusId.ScreenedOut;
178
      const newIsScreenedOut: boolean =
179
        newStatus === ReviewStatusId.ScreenedOut;
180
      return oldIsScreenedOut !== newIsScreenedOut;
181
    };
182
    const oldStatus = application.application_review
183
      ? application.application_review.review_status_id
184
      : null;
185
    if (sectionChange(oldStatus, status)) {
186
      const confirmText =
187
        status === ReviewStatusId.ScreenedOut
188
          ? intl.formatMessage(messages.screenOutConfirm)
189
          : intl.formatMessage(messages.screenInConfirm);
190
      return Swal.fire({
191
        title: confirmText,
192
        icon: "question",
193
        showCancelButton: true,
194
        confirmButtonColor: "#0A6CBC",
195
        cancelButtonColor: "#F94D4D",
196
        confirmButtonText: intl.formatMessage(messages.confirmButton),
197
        cancelButtonText: intl.formatMessage(messages.cancelButton),
198
      }).then(result => {
199
        if (result.value) {
200
          return onStatusChange(application.id, status);
201
        }
202
        return Promise.resolve();
203
      });
204
    }
205
    return onStatusChange(application.id, status);
206
  }
207
208
  protected showNotes(): void {
209
    const { application, onNotesChange, intl } = this.props;
210
    const notes =
211
      application.application_review && application.application_review.notes
212
        ? application.application_review.notes
213
        : "";
214
    Swal.fire({
215
      title: intl.formatMessage(messages.editNote),
216
      icon: "question",
217
      input: "textarea",
218
      showCancelButton: true,
219
      confirmButtonColor: "#0A6CBC",
220
      cancelButtonColor: "#F94D4D",
221
      cancelButtonText: intl.formatMessage(messages.cancelButton),
222
      confirmButtonText: intl.formatMessage(messages.save),
223
      inputValue: notes,
224
    }).then(result => {
225
      if (result && result.value !== undefined) {
226
        const value = result.value ? result.value : null;
227
        onNotesChange(application.id, value);
228
      }
229
    });
230
  }
231
232
  protected handleLinkClicked(url: string): void {
233
    if (this.isUnchanged()) {
234
      window.location.href = url;
235
      return;
236
    }
237
    this.handleSaveClicked()
238
      .then(() => {
239
        window.location.href = url;
240
      })
241
      .catch(() => {
242
        // do not navigate away from page
243
      });
244
  }
245
246
  protected handleStatusChange(
247
    event: React.ChangeEvent<HTMLSelectElement>,
248
  ): void {
249
    const value =
250
      event.target.value && !Number.isNaN(Number(event.target.value))
251
        ? Number(event.target.value)
252
        : undefined;
253
    this.setState({ selectedStatusId: value });
254
  }
255
256
  public render(): React.ReactElement {
257
    const { application, reviewStatusOptions, isSaving, intl } = this.props;
258
    const l10nReviewStatusOptions = reviewStatusOptions.map(status => ({
259
      value: status.value,
260
      label: intl.formatMessage(messages[status.label]),
261
    }));
262
    const { selectedStatusId } = this.state;
263
    const reviewStatus =
264
      application.application_review &&
265
      application.application_review.review_status
266
        ? application.application_review.review_status.name
267
        : null;
268
    const statusIconClass = className("fas", {
269
      "fa-ban": reviewStatus === "screened_out",
270
      "fa-question-circle": reviewStatus === "still_thinking",
271
      "fa-check-circle": reviewStatus === "still_in",
272
      "fa-exclamation-circle": reviewStatus === null,
273
    });
274
275
    const getSaveButtonText = (): string => {
276
      if (isSaving) {
277
        return intl.formatMessage(messages.saving);
278
      }
279
      if (this.isUnchanged()) {
280
        return intl.formatMessage(messages.saved);
281
      }
282
      return intl.formatMessage(messages.save);
283
    };
284
    const saveButtonText = getSaveButtonText();
285
    const noteButtonText =
286
      application.application_review && application.application_review.notes
287
        ? intl.formatMessage(messages.editNote)
288
        : intl.formatMessage(messages.addNote);
289
    return (
290
      <div>
291
        <div>
292
          <div className="manager-application-preview-actions flex-grid">
293
            <div className="box small-1of3">
294
              <button
295
                className="button--blue light-bg"
296
                type="button"
297
                onClick={() =>
298
                  this.handleLinkClicked(
299
                    routes.managerJobApplications(
300
                      intl.locale,
301
                      application.job_poster_id,
302
                    ),
303
                  )
304
                }
305
              >
306
                <FormattedMessage
307
                  id="apl.backToApplicantList"
308
                  defaultMessage="< Save and Go Back to Applicant List"
309
                  description="Back Button text"
310
                />
311
              </button>
312
            </div>
313
            <div className="box small-2of3">
314
              <button
315
                className="button--blue light-bg"
316
                type="button"
317
                onClick={() =>
318
                  this.handleLinkClicked(
319
                    routes.managerJobShow(
320
                      intl.locale,
321
                      application.job_poster_id,
322
                    ),
323
                  )
324
                }
325
              >
326
                <FormattedMessage
327
                  id="button.viewJobPoster"
328
                  defaultMessage="View Job Poster"
329
                  description="View Job Poster Button text"
330
                />
331
              </button>
332
              <button
333
                className="button--blue light-bg"
334
                data-button-type="expand-all"
335
                type="button"
336
                id="expand-all"
337
              >
338
                <span className="expand">
339
                  {" "}
340
                  <FormattedMessage
341
                    id="apl.expandAllSkills"
342
                    defaultMessage="Expand All Skills"
343
                    description="Expand All Skills Button text"
344
                  />
345
                </span>
346
                <span className="collapse">
347
                  {" "}
348
                  <FormattedMessage
349
                    id="apl.collapseAllSkills"
350
                    defaultMessage="Collapse All Skills"
351
                    description="Collapse All Skills Button text"
352
                  />
353
                </span>
354
              </button>
355
            </div>
356
          </div>
357
        </div>
358
359
        <form className="applicant-summary">
360
          <div className="flex-grid middle gutter">
361
            <div className="box lg-1of11 applicant-status">
362
              <i className={statusIconClass} />
363
            </div>
364
365
            <div className="box lg-2of11 applicant-information">
366
              <span className="name">
367
                {application.applicant.user.first_name}{" "}
368
                {application.applicant.user.last_name}
369
              </span>
370
              <a
371
                href={`mailto: ${application.applicant.user.email}`}
372
                title={intl.formatMessage(messages.emailCandidate)}
373
              >
374
                {application.applicant.user.email}
375
              </a>
376
              {/* This span only shown for veterans */}
377
              {(application.veteran_status.name === "current" ||
378
                application.veteran_status.name === "past") && (
379
                <span className="veteran-status">
380
                  <img
381
                    alt={intl.formatMessage(messages.viewApplicationTitle)}
382
                    src="/images/icon_veteran.svg"
383
                  />{" "}
384
                  <FormattedMessage
385
                    id="veteranStatus.veteran"
386
                    defaultMessage="Veteran"
387
                    description="Veteran"
388
                  />
389
                </span>
390
              )}
391
            </div>
392
393
            <div className="box lg-2of11 applicant-links">
394
              <a
395
                title={intl.formatMessage(messages.viewApplicationTitle)}
396
                href={routes.managerApplicationShow(
397
                  intl.locale,
398
                  application.id,
399
                )}
400
              >
401
                <i className="fas fa-file-alt" />
402
                <FormattedMessage
403
                  id="apl.viewApplication"
404
                  defaultMessage="View Application"
405
                  description="Button text View Application"
406
                />
407
              </a>
408
              <a
409
                title={intl.formatMessage(messages.viewProfileTitle)}
410
                href={routes.managerApplicantShow(
411
                  intl.locale,
412
                  application.applicant_id,
413
                )}
414
              >
415
                <i className="fas fa-user" />
416
                <FormattedMessage
417
                  id="apl.viewProfile"
418
                  defaultMessage="View Profile"
419
                  description="Button text View Profile"
420
                />
421
              </a>
422
            </div>
423
424
            <div className="box lg-2of11 applicant-decision" data-clone>
425
              <Select
426
                id={`review_status_${application.id}`}
427
                name="review_status"
428
                label={intl.formatMessage(messages.decision)}
429
                required={false}
430
                selected={selectedStatusId || null}
431
                nullSelection={intl.formatMessage(messages.notReviewed)}
432
                options={l10nReviewStatusOptions}
433
                onChange={this.handleStatusChange}
434
              />
435
            </div>
436
437
            <div className="box lg-2of11 applicant-notes">
438
              <button
439
                className="button--outline"
440
                type="button"
441
                onClick={this.showNotes}
442
              >
443
                {noteButtonText}
444
              </button>
445
            </div>
446
447
            <div className="box lg-2of11 applicant-save-button">
448
              <button
449
                className="button--blue light-bg"
450
                type="button"
451
                onClick={() => this.handleSaveClicked()}
452
              >
453
                <span>{saveButtonText}</span>
454
              </button>
455
            </div>
456
          </div>
457
        </form>
458
      </div>
459
    );
460
  }
461
}
462
463
export default injectIntl(ApplicationReviewWithNav);
464